# == Synopsis
#
# hello: greets user, demonstrates command line parsing
#
# == Usage
#
# hello [OPTION] ... DIR
#
# -h, --help:
#    show help
#
# REQUIRED ARGUMENTS:
# 
# --username, -u [lastfm username]:
#    a valid last.fm username 
#
# --password, -u [lastfm password]:
#    a valid last.fm password 
#
# OPTIONAL ARGUMENTS:
#
# --telnetport, -t [port]:
#    the port to allow telnet access to the lastfm-cli on. 8004 by default. 
#

#
# note this protocal version may be outdated soon
# protocol reference
# http://code.google.com/p/thelastripper/wiki/LastFM12UnofficialDocumentation
# http://www.audioscrobbler.net/development/protocol/
require 'digest/md5'
require 'uri'
require 'net/http'
require 'yaml'
class LastFM
  Handshake = %Q[http://ws.audioscrobbler.com/radio/handshake.php?version=1.1.1&platform=linux&username=%s&passwordmd5=%s&debug=0&partner=]
  Handshake2 = %Q[http://post.audioscrobbler.com/?hs=true&p=1.2&c=tst&v=1.0&u=%s&t=%s&a=%s]
  Tuner = %Q[http://ws.audioscrobbler.com/radio/adjust.php?session=%s&url=lastfm://%s&lang=en]
  CurrentSongInfo = %Q[http://ws.audioscrobbler.com/radio/np.php?session=%s&debug=0]
  Command = %Q[http://ws.audioscrobbler.com/radio/control.php?session=%s&command=%s&debug=0]
  attr_accessor :session, :stream_url, :username, :station_url
  def initialize(options)
    @username, @password = options[:username], options[:password]
    @md5_password = Digest::MD5.hexdigest(@password)
    handshake
  end

  def last_response
    @response
  end

  # version 1.1 protocol
  def handshake
    response = send_request(Handshake % [@username, @md5_password])
    @session = get_property("session", response.body)
    @stream_url= get_property("stream_url", response.body)
  end

  # version 1.2 protocol
  # Currently, we'll use the old protocol.
  def handshake2
    timestamp = Time.now.to_i
    auth_token = Digest::MD5.hexdigest(@md5_password.to_s + timestamp.to_s)
    response = send_request(Handshake2 % [@username, timestamp, auth_token])
    puts response.body
    @session = response.body.split("\n")[1]
    response
  end

  # tuner stations:
  # lastfm://user/<yourlastfmusername>/neighbours
  # lastfm://artist/<Artist>/similarartists
  # lastfm://globaltags/<Tag>
  # For this method, just pass in the path part that varies, e.g.
  # artist/u2/similarartists
  def tune(path=nil)
    @station_url = path || @station_url
    @response = send_request(Tuner % [@session, @station_url])
    return get_property("response", @response.body) == "OK" # return false if failed
  end

  # Info for the current song
  def current_song
    @response = response = send_request(CurrentSongInfo % [@session])
    # puts response.body # for debugging
    streaming = get_property("streaming", response.body) == "true"
    return nil unless streaming
    song = LastFM::Song.new
    LastFM::Song::SongAttributes.each do |attribute|
      value = get_property(attribute.to_s, response.body)
      if value.nil?
        # something went wrong, reset song 
        return nil
      end
      song.send(attribute.to_s + "=", value.strip)
    end
    song
  rescue 
    nil
  end

  # NOTE skip doesn't work for some reason, so we will simply restart the stream
  # from the telnet client
  # skip, love, ban
  def action(command)
    request = Command % [@session, command]
    puts request
    @response = send_request(request)
    puts @response.body
    return get_property("response", @response.body) == "OK" # return false if failed
  end

  # convenience method for parsing the key/value response of the LastFM protocol
  def get_property(key, responsebody)
    re = /^#{key}=(.*)/
    matchdata = re.match(responsebody)
    return unless matchdata
    matchdata[1]
  end

  def send_request(url)
    uri = URI.parse(url)
    Net::HTTP.start(uri.host, uri.port) {|http| http.get(uri.request_uri)}
  end
end

class LastFM
  class Song
    SongAttributes = [:station, :artist, :artist_url, :track, :track_url, :album, :album_url, :albumcover_small, :albumcover_medium, :albumcover_large, :trackduration]
    attr_accessor *SongAttributes
  end

  class Client
    attr_accessor :current_song
    def initialize(lastfm, audio_output)
      @lastfm = lastfm
      @audio_output = audio_output
      @output_buffer = [] 
    end
    def start
      display_current_song
    end
    def parse_command(command, *args)
      if command.nil?
        command = "current song info"
      end
      args = args.join(' ').strip 
      puts "request: #{command} #{args}"

      case command
      when /^h/ 
        @output_buffer << help
      when /^sk/ #skip
        skip 
        @output_buffer << "skipping..."
      when /^st/ #stop
        stop
        @output_buffer << "stopping..."
      when /^pl/ #play
        play
        @output_buffer << "playing..."
      when /^v/
        if args != ''
          @audio_output.volume = args.to_i
          @output_buffer << "setting volume to #{args}" 
        else
          @output_buffer << "current volume: #{@audio_output.volume}"
        end
      when /^l/ #"love"
        @lastfm.action("love")
      when /^b/ # "ban"
        @lastfm.action("ban")
      when /^q/ #"quit"
        @audio_output.stop
        exit 
      when /^a/ # "artist"
        tune("artist/#{args}/similarartists")
      when /^t/ #"tags" or tags
        tune("globaltags/#{args}")
      when /^r/ # "recommended"
        tune("user/#{@lastfm.username}/recommended/100")
      when /^n/ #"neighbors"
        tune("user/#{@lastfm.username}/neighbours")
#      when "tune"
#        tune(args.first)
      else
        display_current_song
      end
    end

    def tune(path)
      @lastfm.tune(path=URI.escape(path))
      @output_buffer << "tuning to #{path}"
      sleep 1
      skip 
    end

    def skip
      @audio_output.skip
      sleep 3 # so that current song has a chance to update
      # The last fm skip command via the Command url doesn't work right now
      #@lastfm.action("skip")
      display_current_song
    end

    def play
      skip
    end

    def stop
      @audio_output.stop
    end

    def divider
      "-" * 80
    end

    def display_current_song
      song = @lastfm.current_song
      if song.nil?
        # use the previous current song
        return "\nNo current song\n" unless @current_song
      else 
        @current_song = song
      end
      @output_buffer << "Currently Playing:"
      @output_buffer << divider
      %W{ artist track album station }.each do |x|
        unless song.respond_to?(x)
          @output_buffer = "something is wrong with the last.fm connection. no song data"
          break
        end
        @output_buffer << x + ": " + song.send(x)
      end
      @output_buffer << divider
      @output_buffer
    end

    def flush_output_buffer
      temp = @output_buffer.dup
      @output_buffer = []
      if temp.class == String
        temp
      else
        temp.join("\n")
      end
    end

    def prompt
      "Press h for help\nlastfm> "
    end

    def help
      %Q[Commands: skip 
          love 
          ban 
          recommended
          artist [artist]   
          tag [tag]        
          tags [tag+tag]  
          volume         
          volume [1-100]
          stop
          play
          quit
          [return]     

          You can truncate the commands to the fewest possible
          unambiguous letters.

          LastFM is sometimes unresponsive. In that case, 
          try "skip" every few seconds until it works again.

lastfm> ] 
    end

    require 'socket'
    class TelnetClient < LastFM::Client
      def initialize(lastfm, audio_output, port)
        @port = port
        super(lastfm, audio_output)
      end
      def start
        server = TCPServer.new('localhost', @port)
        puts
        puts "Starting last.fm telnet server on port #{@port}.

Other computers can log in and control the lastfm stream on this 
computer simultaneously.

For a better experience, try logging in using rlwrap 
(which provides readline support)."
        display_current_song
        while session=server.accept
          puts "A telnet session has just started."
          Thread.new(session) do |my_session|
            my_session.print prompt
            while req = my_session.gets
              puts "\n"
              puts divider
              puts "TELNET REQUEST"
              req = req.split(" ")
              if req[0] =~ /^q/ # quit
                my_session.close
                break
              end
              command = req.shift
              parse_command(command, req) 
              puts divider
              print prompt
              $stdout.flush
              my_session.print flush_output_buffer

              unless command =~ /^h/
                my_session.print "\n" 
                my_session.print prompt 
              end
            end
            puts "A telnet session just ended."
          end
        end
      end
    end
    class CommandLineClient < LastFM::Client
      def initialize(lastfm, audio_output)
        super
      end
      def start
        puts
        #puts "starting command line lastfm client"
        print prompt
        $stdout.flush
        while req = gets
          req = req.split(" ")
          puts 
          command = req.shift
          parse_command(command, req)
          print flush_output_buffer
          unless command =~ /^h/
            puts "\n" 
            print prompt 
          end
          $stdout.flush
        end
        #puts "command line session ended"
      end
    end
  end

  module AudioOutput
    class RAOP
    end

    class ITunes
      require 'osx/cocoa'
      OSX.require_framework 'ScriptingBridge'

      def initialize(stream_url)
        @itunes = OSX::SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
        @stream_url = stream_url
      end

      def quit
        @itunes.stop
      end

      def play
        skip
      end
      def stop
        @itunes.stop
      end
      def skip
        # simply restart the stream
        fork do
          %x{open -g -a iTunes.app #{@stream_url} &}
        end
      end
      def start
        fork do
          %x{open -g -a iTunes.app #{@stream_url} &}
        end
      end
      def volume
        @itunes.soundVolume 
      end
      def volume=(value)
        @itunes.soundVolume = value
      end
    end

    # Code Incomplete
    class MPG123
      def initialize(stream_url)
        @stream_url = stream_url
      end
      def start
        # TODO put this on a separate thread
        command = "mpg123 #{@stream_url}" 
        puts command
        @pipe = IO.popen(command)
        @pid = @pipe.pid
        puts "PID: #{@pid}"
      end
      # no volume control facility
    end
  end
end

def credits
  message = "lastfm-cli : a last.fm ruby client. by daniel choi / betahouse.org / Apr 2008"
  puts "-" * message.length
  puts message
  puts "-" * message.length
end

require 'getoptlong'
require 'rdoc/usage'
def parse_options
  opts = GetoptLong.new(
    [ "--username", "-u", GetoptLong::REQUIRED_ARGUMENT],
    [ "--password", "-p", GetoptLong::REQUIRED_ARGUMENT],
    [ "--help", "-h", GetoptLong::OPTIONAL_ARGUMENT],
    [ "--telnetport", "-t", GetoptLong::OPTIONAL_ARGUMENT]
  )

  username, password, telnetport  = nil, nil, 8004
  print "LastFM Username: "
  username = STDIN.gets.chomp 
  print "LastFM Password (will remain hidden): "
  `stty -echo`
  password = STDIN.gets.chomp ensure `stty echo`
  puts
  print "Telnet port (default is 8004): "
  telnetport = STDIN.gets.chomp  
  telnetport = if telnetport == "" 
    8004
  else
    telnetport.to_i
  end

  if username.nil? || password.nil?
    puts
    puts "* MISSING SOME REQUIRED ARGUMENTS *"
  
    exit
  end
  lastfm = LastFM.new(:username => username, :password => password)
  audio_output = LastFM::AudioOutput::ITunes.new(lastfm.stream_url)
  audio_output.start
  puts "starting iTunes radio..."
  puts "Please be patient.\nIt may take some time to connect to the lastfm stream."
  Thread.new do
    client = LastFM::Client::TelnetClient.new(lastfm, audio_output, telnetport)
    #client = LastFM::Client::TelnetClient.new(lastfm, audio_output, 8004)
    client.start
  end
  client2 = LastFM::Client::CommandLineClient.new(lastfm, audio_output)
  client2.start

end
credits # print credits
parse_options


